/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { BaseChecker, BaseMetaData } from "../BaseChecker";
import { ts,  ArkFile} from 'arkanalyzer';
import { Defects, FileMatcher, MatcherCallback, MatcherTypes, Rule } from '../../Index';
import path from 'path';
import { RuleListUtil } from "../../utils/common/DefectsList";
import { IssueReport } from "../../model/Defects";

enum Usefulness {
    Always = 'always',
    Never = 'will',
    Sometimes = 'may',
}
interface Options {
    ignoredTypeNames?: string[];
}

export class NoBaseToStringCheck implements BaseChecker {
    private reportedNodes = new Map<string, Usefulness>();
    private sourceMap = new Map<string, string[]>();
    private safeVariables: Set<string> = new Set();
    private readonly FUNCTION_REGEX = /^(\(?\(\)\s*=>\s*\{.*\}\)?|function\s*\(.*\)\s*\{.*\})\)?$/;
    private readonly LITERAL_TO_STRING_REGEX = /(?:\(\s*\{\s*\}\s*\)|\{\s*\})\s*\.toString\(/;
    readonly metaData: BaseMetaData = {
        severity: 2,
        ruleDocPath: 'docs/no-base-to-string-check.md',
        description: 'Require .toString() to only be called on objects which provide useful information when stringified.',
    };
    private dangerousUnionVariables: Set<string> = new Set();
    public rule: Rule;
    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private options: Options = {
        ignoredTypeNames: ['Error', 'RegExp', 'URL', 'URLSearchParams', 'object', 'Object']
    } as Required<Options>;
    private checker?: ts.TypeChecker;
    constructor() {
        this.dangerousUnionVariables = new Set();
    }
    registerMatchers(): MatcherCallback[] {
        const matchFileCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check.bind(this)
        };
        return [matchFileCb];
    }

    public check(arkFile: ArkFile): void {
        this.options = this.rule && this.rule.option[0] ? this.rule.option as Options : this.options;
        try {
            this.checker = undefined;
            this.dangerousUnionVariables.clear();
            this.safeVariables.clear();
            const filePath = arkFile.getFilePath();
            const code = arkFile.getCode();
            this.sourceMap.set(arkFile.getFilePath(), code.split('\n'));
            const sourceFile = ts.createSourceFile(
                arkFile.getFilePath(),
                code,
                ts.ScriptTarget.Latest,
                true
            );

            const program = ts.createProgram({
                rootNames: [arkFile.getFilePath()],
                options: {
                    skipDefaultLibCheck: true,
                    skipLibCheck: true
                },
                host: ts.createCompilerHost({})
            });

            this.checker = program.getTypeChecker();
            this.traverseNodes(filePath, sourceFile);

        } catch (err) {
           return;
        }

    }

    codeFix?(arkFile: ArkFile, fixKey: string): boolean {
        throw new Error("Method not implemented.");
    }

    private traverseNodes(filePath: string, node: ts.Node): void {
        try {
            this.trackDynamicConversion(node);
            this.trackExplicitConversion(node);

            if (ts.isExpressionStatement(node) &&
                ts.isBinaryExpression(node.expression) &&
                node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
            ) {
                const left = node.expression.left;
                const right = node.expression.right;
                if (ts.isIdentifier(left) && this.isSafeStringConversion(right)) {
                    this.markVariableAsSafe(left.text, node);
                }
            }
            if (ts.isVariableDeclaration(node) && node.initializer) {
                if (this.isSafeStringConversion(node.initializer)) {
                    this.markVariableAsSafe(node.name.getText(), node);
                }
            }
            if (ts.isVariableDeclaration(node)) {
                this.checkVariableDeclaration(filePath, node);
            }
            if (ts.isBinaryExpression(node) &&
                (node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken)) {
                this.checkPlusExpression(filePath, node);
            }

            if (ts.isTemplateExpression(node)) {
                node.templateSpans.forEach(span => {
                    const exprType = this.getTypeAtLocationSafe(span.expression);
                    if (ts.isObjectLiteralExpression(span.expression)) {
                        this.reportDefect(filePath, span.expression, Usefulness.Never);
                    } else if (exprType) {
                        this.validateExpression(filePath, span.expression, exprType);
                    }
                });
            }
            if (ts.isCallExpression(node) &&
                ts.isPropertyAccessExpression(node.expression) &&
                node.expression.name.text === 'toString') {

                const callText = node.getText().replace(/\s+/g, '');
                if (this.LITERAL_TO_STRING_REGEX.test(callText)) {
                    this.reportDefect(
                        filePath,
                        node,
                        Usefulness.Never
                    );
                    return;
                }
            }
            if (
                ts.isCallExpression(node) &&
                node.expression.getText().endsWith('.map')
            ) {
                return;
            }
            if (ts.isTemplateExpression(node)) {
                node.templateSpans.forEach(span => {
                    const exprType = this.getTypeAtLocationSafe(span.expression);
                    if (exprType) {
                        this.validateExpression(filePath, span.expression, exprType);
                    }
                });
            }

            if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
                this.checkPlusExpression(filePath, node);
            }

            if (ts.isCallExpression(node) &&
                ts.isPropertyAccessExpression(node.expression) &&
                node.expression.name.text === 'toString') {
                this.checkToStringCall(filePath, node.expression.expression);
            }
            ts.forEachChild(node, (childNode) => {
                this.traverseNodes(filePath, childNode);
            });

        } catch (e) {
           return;
        }
    }

    private trackDynamicConversion(node: ts.Node) {
        if (ts.isIfStatement(node)) {
            ts.forEachChild(node, child => {
                if (ts.isBlock(child)) {
                    child.statements.forEach(stmt => {
                        if (ts.isExpressionStatement(stmt) &&
                            ts.isBinaryExpression(stmt.expression) &&
                            stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
                        ) {
                            const left = stmt.expression.left;
                            const right = stmt.expression.right;
                            if (ts.isIdentifier(left) &&
                                ts.isCallExpression(right) &&
                                right.expression.getText() === 'JSON.stringify'
                            ) {
                                this.markVariableAsSafe(left.text, node);
                                const currentScope = this.checker!.getSymbolAtLocation(left);
                                if (currentScope) {
                                    currentScope.members?.forEach(symbol => {
                                        (symbol as any).type = this.checker!.getTypeAtLocation(
                                            ts.factory.createStringLiteral('')
                                        );
                                    });
                                }
                            }
                        }
                    });
                }
            });
        }
    }

    private isSafeStringConversion(expr: ts.Node): boolean {
        return ts.isCallExpression(expr) &&
            (expr.expression.getText() === 'JSON.stringify' ||
                expr.expression.getText() === 'String');
    }

    private markVariableAsSafe(varName: string, node: ts.Node) {
        this.dangerousUnionVariables.delete(varName);
        this.safeVariables.add(varName);

        const symbol = this.checker?.getSymbolsInScope(
            node.getSourceFile(),
            ts.SymbolFlags.Variable
        )?.find(s => s.name === varName);

        if (symbol) {
            const stringType = this.checker!.getTypeAtLocation(
                ts.factory.createStringLiteral('')
            );

            (symbol as any).flags |= ts.SymbolFlags.Transient;
            (symbol as any).type = stringType;

            const declarations = symbol.getDeclarations() || [];
            declarations.forEach(decl => {
                if (ts.isVariableDeclaration(decl)) {
                    (decl as any).type = ts.factory.createKeywordTypeNode(
                        ts.SyntaxKind.StringKeyword
                    );
                }
            });
        }
    }

    private checkToStringCall(filePath: string, expr: ts.Node): void {
        if (!this.checker) return;
        if (ts.isIdentifier(expr)) {
            const symbol = this.checker!.getSymbolAtLocation(expr);
            if (symbol) {
                symbol.declarations?.forEach(decl => {
                    if (ts.isVariableDeclaration(decl) && decl.initializer) {
                        const trueType = this.checker!.getTypeAtLocation(decl.initializer);
                        if (trueType?.isUnion()) {
                            trueType.types.forEach(t => {
                                if (this.checkToStringUsefulness(t, filePath, expr) === Usefulness.Never) {
                                    this.reportDefect(filePath, expr, Usefulness.Sometimes);
                                }
                            });
                        }
                    }
                });
            }
        }
        const exprType = this.getTypeAtLocationSafe(expr);
        if (!exprType) return;

        const usefulness = this.checkToStringUsefulness(exprType, expr.getSourceFile().fileName, expr);
        if (usefulness !== Usefulness.Always) {
            this.reportDefect(
                expr.getSourceFile().fileName,
                expr,
                usefulness
            );
        }
    }

    private handleObjectLiteral(type: ts.Type): Usefulness {
        const toStringDeclarations = this.checker?.getPropertyOfType(type, 'toString')?.getDeclarations();
        if (type.isClassOrInterface() && type.getProperties().length === 0) {
            return Usefulness.Never;
        }
        if (!toStringDeclarations?.some(decl =>
            !ts.isInterfaceDeclaration(decl.parent) ||
            decl.parent.name.text !== 'Object'
        )) {
            return Usefulness.Never;
        }

        return Usefulness.Always;
    }

    private checkPlusExpression(filePath: string, node: ts.BinaryExpression): void {
        if (!this.checker) {
            return;
        }
        const checkOperandTypes = (operand: ts.Node) => {
            const operandType = this.getTypeAtLocationSafe(operand);
            if (!operandType) return;
            const typeStack: ts.Type[] = [operandType];
            while (typeStack.length > 0) {
                const currentType = typeStack.pop()!;

                if (currentType.isUnionOrIntersection()) {
                    typeStack.push(...currentType.types);
                    continue;
                }

                const usefulness = this.checkToStringUsefulness(currentType, filePath, operand);
                if (usefulness !== Usefulness.Always) {
                    this.reportDefect(filePath, operand, usefulness);
                }
            }
        };

        checkOperandTypes(node.left);
        checkOperandTypes(node.right);
        const checkUnionOperand = (operand: ts.Node) => {
            const operandType = this.getTypeAtLocationSafe(operand);
            if (operandType?.isUnion()) {
                operandType.types.forEach(t => {
                    if (this.checkToStringUsefulness(t, filePath, operand) === Usefulness.Never) {
                        this.reportDefect(filePath, operand, Usefulness.Sometimes);
                    }
                });
            }
        };
        checkUnionOperand(node.left);
        checkUnionOperand(node.right);
        const deepCheckOperand = (operand: ts.Node) => {
            const operandType = this.getTypeAtLocationSafe(operand);
            if (!operandType) return;
            if (operandType.isUnion()) {
                operandType.types.forEach(t => {
                    const nestedUsefulness = this.checkToStringUsefulness(t, filePath, operand);
                    if (nestedUsefulness !== Usefulness.Always) {
                        this.reportDefect(filePath, operand, nestedUsefulness);
                    }
                });
                return;
            }
            const checkNestedTypes = (t: ts.Type) => {
                if (t.isUnionOrIntersection()) {
                    t.types.forEach(checkNestedTypes);
                } else {
                    const usefulness = this.checkToStringUsefulness(t, filePath, operand);
                    if ([Usefulness.Never, Usefulness.Sometimes].includes(usefulness)) {
                        this.reportDefect(filePath, operand, usefulness);
                    }
                }
            };

            checkNestedTypes(operandType);
        };
        const leftType = this.getTypeAtLocationSafe(node.left);
        const rightType = this!.getTypeAtLocationSafe(node.right);
        if (!leftType || !rightType) return;
        deepCheckOperand(node.left);
        deepCheckOperand(node.right);
        if (node.operatorToken.kind === ts.SyntaxKind.PlusEqualsToken) {
            const leftType = this.getTypeAtLocationSafe(node.left);
            if (leftType && this.getTypeName(leftType) === 'string') {
                this.validateExpression(filePath, node.right, this.getTypeAtLocationSafe(node.right)!);
            }
        }
        const checkOperand = (operand: ts.Node) => {
            const operandType = this.getTypeAtLocationSafe(operand);
            if (!operandType) return;

            const usefulness = this.checkToStringUsefulness(operandType, filePath, operand);
            if (usefulness === Usefulness.Never) {
                this.reportDefect(filePath, operand, usefulness);
            } else if (usefulness === Usefulness.Sometimes) {
                if (ts.isIdentifier(operand) && this.isUnionWithObjectType(operandType)) {
                    this.reportDefect(filePath, operand, usefulness);
                }
            }
        };

        checkOperand(node.left);
        checkOperand(node.right);

        const leftName = this.getTypeName(leftType);
        const rightName = this.getTypeName(rightType);

        if (leftName === 'string') {
            this.validateExpression(filePath, node.right, rightType);
        } else if (rightName === 'string') {
            this.validateExpression(filePath, node.left, leftType);
        }
    }
    private isUnionWithObjectType(type: ts.Type): boolean {
        if (type.isUnion()) {
            return type.types.some(t =>
                this.getTypeName(t).includes('{') ||
                this.checkToStringUsefulness(t, '', ts.factory.createIdentifier('')) === Usefulness.Never
            );
        }
        return false;
    }

    private validateExpression(filePath: string, node: ts.Node, type: ts.Type): void {
        if (ts.isIdentifier(node) && this.safeVariables.has(node.text)) {
            return;
        }
        if (ts.isIdentifier(node) && this.isExplicitlyConverted(node)) {
            return;
        }
        if (ts.isIdentifier(node) &&
            !this.dangerousUnionVariables.has(node.text) &&
            this.checker!.typeToString(type) === 'string'
        ) {
            return;
        }
        if (ts.isIdentifier(node) && this.dangerousUnionVariables.has(node.text)) {
            this.reportDefect(filePath, node, Usefulness.Sometimes);
            return;
        }
        if (ts.isIdentifier(node)) {
            if (this.safeVariables.has(node.text)) {
                return;
            }

            const currentType = this.checker!.getTypeAtLocation(node);
            if (this.checker!.typeToString(currentType) === 'string') {
                return;
            }
        }
        if (type.isUnion()) {
            type.types.forEach(t => {
                if (this.checkToStringUsefulness(t, filePath, node) === Usefulness.Never) {
                    this.reportDefect(filePath, node, Usefulness.Sometimes);
                }
            });
            return;
        }

        if (ts.isParameter(node) &&
            node.type?.getText().match(/\{\s*toString\s*\(\)\s*:\s*.*?\s*\}/)
        ) {
            return;
        }
        if (ts.isParameter(node) &&
            node.type?.getText().includes('{ toString(): any }')) {
            return;
        }
        if (ts.isCallExpression(node) &&
            node.expression.getText().endsWith('.constructor')) {
            return;
        }
        if (ts.isParameter(node.parent) &&
            ts.isFunctionTypeNode(node.parent.parent) &&
            node.parent.type?.getText().includes('toString')
        ) {
            return;
        }

        if (ts.isVariableDeclaration(node.parent) &&
            node.parent.type?.getText().includes('{ toString(): any }')
        ) {
            return;
        }
        if (ts.isCallExpression(node.parent) &&
            ts.isPropertyAccessExpression(node.parent.expression) &&
            node.parent.expression.name?.text === 'constructor') {
            return;
        }

        if (ts.isNewExpression(node.parent) &&
            ts.isPropertyAccessExpression(node.parent.expression) &&
            node.parent.expression.name.text === 'constructor') {
            return;
        }

        if (ts.isTemplateSpan(node.parent) && ts.isTemplateExpression(node.parent.parent)) {
            const fullTemplateText = node.parent.parent.getText();
            const boolTemplateRegex = /\$\{\s*!*(true|false)\s*\}/g;
            if (boolTemplateRegex.test(fullTemplateText)) {
                return;
            }
        }
        const nodeText = node.getText().replace(/\s+/g, '');
        if (
            this.FUNCTION_REGEX.test(nodeText) ||
            /\.map\(.*=>/.test(nodeText)
        ) {
            return;
        }
        if (ts.isNewExpression(node.parent) &&
            ts.isPropertyAccessExpression(node.parent.expression) &&
            node.parent.expression.name.text === 'constructor') {
            return;
        }
        if (node.getText().includes('.constructor()')) {
            return;
        }

        if (ts.isArrowFunction(node.parent) || ts.isFunctionExpression(node.parent)) {
            return;
        }
        if (!type || !this.checker || type.flags & ts.TypeFlags.Any) {
            return;
        }

        if (ts.isLiteralTypeNode(node)) return;
        const typeName = this.getTypeName(type).replace(/\(.*\)/g, ''); 
        if (this.options.ignoredTypeNames?.includes(typeName)) {
            return;
        }
        if (ts.isIdentifier(node)) {
            const symbol = this.checker!.getSymbolAtLocation(node);
            if (symbol) {
                const declarations = symbol.getDeclarations() || [];
                declarations.forEach(decl => {
                    if (ts.isVariableDeclaration(decl) && decl.initializer) {
                        const initType = this.checker!.getTypeAtLocation(decl.initializer);
                        if (initType && this.checkToStringUsefulness(initType, filePath, decl.initializer) === Usefulness.Never) {
                            this.reportDefect(filePath, node, Usefulness.Never);
                        }
                    }
                });
            }
        }

        if (ts.isBinaryExpression(node.parent) &&
            node.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
            ts.isBinaryExpression(node.parent.parent)) {
            const symbol = this.checker!.getSymbolAtLocation(node.parent.left);
            if (symbol) {
                symbol.declarations?.forEach(decl => {
                    if (ts.isVariableDeclaration(decl) && decl.initializer) {
                        const initType = this.checker!.getTypeAtLocation(decl.initializer);
                        if (initType && this.checkToStringUsefulness(initType, filePath, decl.initializer) === Usefulness.Never) {
                            if (ts.isBinaryExpression(node.parent)) {
                                this.reportDefect(filePath, node.parent.left, Usefulness.Never);
                            }
                        }
                    }
                });
            }
        }
        const usefulness = this.checkToStringUsefulness(type, filePath, node);
        if (usefulness !== Usefulness.Always) {
            this.reportDefect(filePath, node, usefulness);
        }
    }
    private isExplicitlyConverted(node: ts.Identifier): boolean {
        const symbol = this.checker?.getSymbolAtLocation(node);
        return !!symbol?.declarations?.some(decl =>
            ts.isVariableDeclaration(decl) &&
            decl.initializer &&
            this.isSafeStringConversion(decl.initializer)
        );
    }
    private checkToStringUsefulness(type: ts.Type, filePath: string, node: ts.Node): Usefulness {
        if (this.checker!.typeToString(type) === 'RegExp') {
            return Usefulness.Always;
        }
        const typeSymbolName = type.getSymbol()?.getName();
        if (typeSymbolName && this.options.ignoredTypeNames?.includes(typeSymbolName)) {
            return Usefulness.Always;
        }
        if (type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean)) {
            return Usefulness.Always;
        }
        if (type.getFlags() & ts.TypeFlags.Object) {
            const builtInTypes = ['Date', 'Error', 'URL', 'RegExp'];
            if (builtInTypes.some(t => this.checker!.typeToString(type).startsWith(t))) {
                return Usefulness.Always;
            }

            const toStringSymbol = this.checker?.getPropertyOfType(type, 'toString');
            const hasCustomToString = toStringSymbol?.declarations?.some(decl => {
                const parent = decl.parent;
                return !ts.isInterfaceDeclaration(parent) || parent.name.text !== 'Object';
            });

            return hasCustomToString ? Usefulness.Always : Usefulness.Never;
        }
        if (ts.isIdentifier(node)) {
            const symbol = this.checker?.getSymbolAtLocation(node);
            if (symbol?.declarations?.some(d =>
                ts.isVariableDeclaration(d) &&
                d.initializer &&
                this.isSafeStringConversion(d.initializer)
            )) {
                return Usefulness.Always;
            }
        }
        if (type.isUnion()) {
            const types = type.types;
            let hasNever = false;
            let hasAlways = false;

            for (const t of types) {
                const result = this.checkToStringUsefulness(t, filePath, node);
                if (result === Usefulness.Always) {
                    if (this.hasExplicitConversion(node)) {
                        return Usefulness.Always;
                    }
                    hasAlways = true;
                } else if (result === Usefulness.Never) {
                    hasNever = true;
                }
            }
            if (hasAlways && this.hasExplicitConversion(node)) {
                return Usefulness.Always;
            }

            if (hasNever && hasAlways) return Usefulness.Sometimes;
            if (hasNever) return Usefulness.Never;
            return hasAlways ? Usefulness.Always : Usefulness.Never;
        }

        if (type.getFlags() & ts.TypeFlags.Object &&
            type.getSymbol()?.getName() === '__type' &&
            type.getProperties().length === 0
        ) {
            return Usefulness.Never;
        }
        if (ts.isConditionalExpression(node.parent) &&
            this.isObjectLikeType(type)
        ) {
            return Usefulness.Never;
        }
        if (ts.isConditionalExpression(node.parent)) {
            return Usefulness.Never;
        }

        if (ts.isObjectLiteralExpression(node) && node.properties.length === 0) {
            return Usefulness.Never;
        }
        if (ts.isObjectLiteralExpression(node)) {
            return Usefulness.Never;
        }
        if (type.flags & ts.TypeFlags.Object &&
            type.getSymbol()?.getName() === 'Object') {
            return Usefulness.Always;
        }
        if (ts.isObjectLiteralExpression(node) && node.properties.length === 0) {
            return Usefulness.Never;
        }

        const typeName = this.checker?.typeToString(type);
        if (typeName === 'Object') {
            return Usefulness.Always;
        }

        const constructorType = this.checker?.getTypeAtLocation(
            ts.factory.createIdentifier("Object")
        );
        if (constructorType && type.getSymbol() === constructorType.getSymbol()) {
            return Usefulness.Always;
        }

        if (type.getSymbol()?.getName() === 'Object' ||
            type.getSymbol()?.getEscapedName() === 'Object') {
            return Usefulness.Always;
        }

        if (this.checker?.getPropertyOfType(type, 'toString')?.getDeclarations()?.some(d =>
            !ts.isInterfaceDeclaration(d.parent) || d.parent.name.text !== 'Object'
        )) {
            return Usefulness.Always;
        }
        if (type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number | ts.TypeFlags.Boolean)) {
            return Usefulness.Always;
        }
        if (type.flags & (ts.TypeFlags.Boolean | ts.TypeFlags.BooleanLiteral)) {
            return Usefulness.Always;
        }

        if (type.flags & (ts.TypeFlags.String | ts.TypeFlags.Number)) {
            return Usefulness.Always;
        }

        if (type.isUnionOrIntersection()) {
            const types = type.isUnion() ? type.types : (type as ts.IntersectionType).types;
            const results = types.map(t => this.checkToStringUsefulness(t, filePath, node));
            const hasNever = results.some(r => r === Usefulness.Never);
            const hasAlways = results.some(r => r === Usefulness.Always);

            if (hasNever && hasAlways) return Usefulness.Sometimes;
            if (hasNever) return Usefulness.Never;
            return hasAlways ? Usefulness.Always : Usefulness.Never;
        }

        if (ts.isIdentifier(node)) {
            const symbol = this.checker!.getSymbolAtLocation(node);
            if (symbol) {
                const declarations = symbol.getDeclarations() || [];
                const conditionalTypes = declarations
                    .filter(ts.isVariableDeclaration)
                    .flatMap(d => {
                        const initType = d.initializer && this.checker!.getTypeAtLocation(d.initializer);
                        return initType?.isUnion() ? initType.types : [];
                    });

                if (conditionalTypes.some(t =>
                    this.checkToStringUsefulness(t, filePath, node) === Usefulness.Never
                )) {
                    return Usefulness.Sometimes;
                }
            }
        }
        if (ts.isObjectLiteralExpression(this.unwrapParentheses(node))) {
            const unwrappedNode = this.unwrapParentheses(node);
            return ts.isObjectLiteralExpression(unwrappedNode) ?
                (unwrappedNode.properties.length === 0 ? Usefulness.Never : this.handleObjectLiteral(type)) :
                Usefulness.Never;
        }

        if (ts.isIdentifier(node) && this.isDynamicAssignment(node)) {
            const symbol = this.checker!.getSymbolAtLocation(node);
            if (symbol) {
                const declarations = symbol.getDeclarations() || [];
                const declarationTypes = declarations.map(d =>
                    this.checker!.getTypeAtLocation(d)
                );
                if (declarationTypes.some(t =>
                    this.checkToStringUsefulness(t, filePath, node) === Usefulness.Never
                )) {
                    return Usefulness.Never;
                }
            }
        }

        if (typeName && this.options.ignoredTypeNames?.includes(typeName)) {
            return Usefulness.Always;
        }

        const toStringMethod = this.checker?.getPropertyOfType(type, 'toString');
        const declarations = toStringMethod?.getDeclarations();
        if (!declarations) return Usefulness.Never;

        const hasValidToString = declarations?.some(decl => {
            const parent = decl.parent;
            return !ts.isInterfaceDeclaration(parent) || parent.name.text !== 'Object';
        });

        return hasValidToString ? Usefulness.Always : Usefulness.Never;
    }
    private hasExplicitConversion(node: ts.Node): boolean {
        if (!ts.isIdentifier(node)) return false;

        const symbol = this.checker?.getSymbolAtLocation(node);
        return !!symbol?.declarations?.some(decl =>
            ts.isVariableDeclaration(decl) &&
            decl.initializer &&
            this.isSafeStringConversion(decl.initializer)
        );
    }
    private checkVariableDeclaration(filePath: string, node: ts.VariableDeclaration): void {
        const varType = this.checker!.getTypeAtLocation(node);
        if (!varType) return;
        const checkType = (t: ts.Type): boolean =>
            this.checkToStringUsefulness(t, filePath, node) === Usefulness.Never;

        if (varType.isUnionOrIntersection()) {
            const types = varType.types;
            if (types.some(checkType)) {
                this.dangerousUnionVariables.add(node.name.getText());
            }
        } else if (checkType(varType)) {
            this.dangerousUnionVariables.add(node.name.getText());
        }
    }
    private isObjectLikeType(type: ts.Type): boolean {
        return type.getSymbol()?.getName() === 'Object' ||
            (type.getFlags() & ts.TypeFlags.Object) !== 0;
    }
    private unwrapParentheses(node: ts.Node): ts.Node {
        let current = node;
        while (ts.isParenthesizedExpression(current)) {
            current = current.expression;
        }
        return current;
    }

    private isDynamicAssignment(node: ts.Identifier): boolean {
        const symbol = this.checker!.getSymbolAtLocation(node);
        return !!symbol?.declarations?.some(decl =>
            ts.isVariableDeclaration(decl) &&
            decl.initializer &&
            ts.isConditionalExpression(decl.initializer)
        );
    }
    private getTypeName(type: ts.Type): string {
        if (!this.checker || !type) {
            return 'unknown';
        }
        return this.checker.typeToString(type);
    }
    private trackExplicitConversion(node: ts.Node) {
        if (ts.isIfStatement(node)) {
            ts.forEachChild(node, child => {
                if (ts.isBlock(child)) {
                    child.statements.forEach(stmt => {
                        if (ts.isExpressionStatement(stmt) &&
                            ts.isBinaryExpression(stmt.expression) &&
                            stmt.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
                        ) {
                            const left = stmt.expression.left;
                            const right = stmt.expression.right;
                            if (ts.isIdentifier(left) && this.isSafeStringConversion(right)) {
                                this.markVariableAsSafe(left.text, node);
                            }
                        }
                    });
                }
            });
        }
    }

    private getTypeAtLocationSafe(node: ts.Node): ts.Type | undefined {
        try {
            const nodeText = node.getText().replace(/\s+/g, '');
            if (this.FUNCTION_REGEX.test(nodeText)) {
                return undefined;
            }
            const deepUnwrap = (n: ts.Node): ts.Node => {
                let current = n;
                while (ts.isParenthesizedExpression(current)) {
                    current = current.expression;
                }
                if (ts.isArrowFunction(current) && ts.isParenthesizedExpression(current.parent)) {
                    return deepUnwrap(current.parent);
                }
                return current;
            };

            const targetNode = deepUnwrap(node);
            if (ts.isJSDoc(targetNode) || ts.isTypeNode(targetNode)) {
                return undefined;
            }
            if (ts.isFunctionExpression(targetNode) || ts.isArrowFunction(targetNode)) {
                const signatures = this.checker!.getSignaturesOfType(
                    this.checker!.getTypeAtLocation(targetNode),
                    ts.SignatureKind.Call
                );
                return signatures[0]?.getReturnType() ||
                    this.checker!.getTypeAtLocation(ts.factory.createIdentifier("any")) ||
                    { flags: ts.TypeFlags.Any } as ts.Type;
            }
            return this.checker!.getTypeAtLocation(targetNode);
        } catch (e) {
            return undefined;
        }
    }

    private reportDefect(filePath: string, node: ts.Node, certainty: Usefulness): void {
        const normalizedPath = filePath.replace(/\\/g, '/');
        const sourceFile = node.getSourceFile();
        const startPos = node.getStart();
        const { line, character: startCol } = ts.getLineAndCharacterOfPosition(sourceFile, startPos);
        let adjustedCol = startCol + 1;
        if(ts.isCallExpression(node) && node.expression.getText().replace(/\s+/g, '') === '({}).toString'){
            adjustedCol += 1;
        }
        const nodeKey = `${path.basename(normalizedPath)}:${line + 1}:${line + 1}:${certainty}`;

        if (this.reportedNodes.has(nodeKey)) return;
        this.reportedNodes.set(nodeKey, certainty);

        const rawText = node.getText().trim();
        const normalizedText = rawText.replace(/\s+/g, '').replace(/\?\./g, '.');

        let description = `${this.getCleanDisplayText(node)} `;
        description += certainty === Usefulness.Never
            ? "will evaluate to '[object Object]'"
            : "may evaluate to '[object Object]'";
        description += " when stringified";
        if (ts.isObjectLiteralExpression(node) && node.properties.length === 0) {
            description = `${description}`; 
        }

        const originalLine = this.getOriginalLineNumber(normalizedPath, startPos);
        const endCol = startCol + (node.getEnd() - node.getStart());
        const positionKey = `${path.normalize(normalizedPath)}:${originalLine}:${startCol}:${endCol}`;
        if (this.reportedNodes.has(positionKey)) return;
        this.reportedNodes.set(positionKey, certainty);

        if (normalizedText === '{}' && certainty !== Usefulness.Never) return;

        const fixKey = originalLine + '%' + startCol + '%' + this.rule.ruleId;
        const mergeKey = normalizedPath + '%' + fixKey + '%' + description;
        const defect = new Defects(originalLine, adjustedCol, description, this.metaData.severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey)
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }
    private getCleanDisplayText(node: ts.Node): string {
        const rawText = node.getText().trim();

        if (ts.isCallExpression(node) &&
            node.expression.getText().replace(/\s+/g, '') === '({}).toString') {
            return '\'{}\'';
        }

        if (rawText === '{}') return '\'{}\'';

        if (ts.isIdentifier(node)) {
            return `'${rawText}'`;
        }

        return rawText.length > 20
            ? `'${rawText.slice(0, 17)}...'`
            : `'${rawText}'`;
    }

    private getOriginalLineNumber(filePath: string, pos: number): number {
        const normalizedPath = path.normalize(filePath);
        const lines = this.sourceMap.get(normalizedPath);
        if (!lines || pos < 0) return 0;
        let offset = 0;
        if (lines.length > 0 && lines[0].charCodeAt(0) === 0xFEFF) {
            offset = 1;
        }

        let currentPos = offset;
        for (let lineNum = 0; lineNum < lines.length; lineNum++) {
            const lineLength = Math.max(lines[lineNum].length, 0);
            const lineEnd = currentPos + lineLength + 1;

            if (pos < lineEnd || lineNum === lines.length - 1) {
                return lineNum + 1;
            }
            currentPos = lineEnd;
        }
        return lines.length;
    }

}